﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Ast.Framework.Environment.Extensions.Loaders;
using Ast.Framework.FileSystems.Dependencies;
using Ast.Framework.FileSystems.Paths;
using Ast.Framework.Localization;
using Castle.Core.Logging;
using Microsoft.CSharp;

namespace Ast.Framework.Environment.Extensions.Compilers
{
    public class DefaultExtensionCompiler : IExtensionCompiler
    {
        private readonly IPathProvider _pathProvider;
        private readonly IDependenciesFolder _dependenciesFolder;
        private readonly IProjectFileParser _projectFileParser;
        private readonly IEnumerable<IExtensionLoader> _loaders;
        private readonly IAssemblyLoader _assemblyLoader;
        private readonly ICriticalErrorProvider _criticalErrorProvider;

        public DefaultExtensionCompiler(IPathProvider pathProvider, IDependenciesFolder dependenciesFolder, IProjectFileParser projectFileParser, IEnumerable<IExtensionLoader> loaders, IAssemblyLoader assemblyLoader, ICriticalErrorProvider criticalErrorProvider)
        {
            _pathProvider = pathProvider;
            _dependenciesFolder = dependenciesFolder;
            _projectFileParser = projectFileParser;
            _loaders = loaders;
            _assemblyLoader = assemblyLoader;
            _criticalErrorProvider = criticalErrorProvider;

            T = NullLocalizer.Instance;
            Logger = NullLogger.Instance;
        }

        public Localizer T { get; set; }

        public ILogger Logger { get; set; }

        public Assembly Compile(CompileExtensionContext context)
        {
            Logger.InfoFormat("Generate code for file \"{0}\"", context.Path);
            var moduleName = Path.GetFileNameWithoutExtension(context.Path);
            var dependencyDescriptor = _dependenciesFolder.GetDescriptor(moduleName);
            if (dependencyDescriptor == null) return null;

            try
            {
                var projectFileDescriptor = _projectFileParser.Parse(context.Path);

                // add source files
                var compileUnits = new List<CodeCompileUnit>();
                var directory = _pathProvider.GetDirectoryName(context.Path);
                foreach (var filename in projectFileDescriptor.SourceFilenames.Select(f => _pathProvider.Combine(directory, f)))
                {
                    compileUnits.Add(CreateCompileUnit(filename));
                }

                var addedReferences = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                var references = new List<string>();

                // Add assembly references
                foreach (var reference in dependencyDescriptor.References)
                {
                    var referenceTemp = reference;
                    var loader = _loaders.SingleOrDefault(l => l.Name == referenceTemp.LoaderName);
                    if (loader == null)
                    {
                        Logger.WarnFormat("Could not find loader '{0}' in active loaders", reference.LoaderName);
                        continue;
                    }

                    var assembly = loader.LoadReference(reference);
                    if (assembly == null)
                    {
                        Logger.WarnFormat("Loader '{0}' could not load reference '{1}'", reference.LoaderName, reference.Name);
                        continue;
                    }

                    addedReferences.Add(reference.Name);
                    references.Add(assembly.Location);
                }

                // Load references specified in project file (only the ones not yet loaded)
                foreach (var assemblyReference in projectFileDescriptor.References)
                {
                    if (addedReferences.Contains(assemblyReference.SimpleName))
                        continue;

                    var assembly = _assemblyLoader.Load(assemblyReference.FullName);
                    if (assembly != null)
                    {
                        references.Add(assembly.Location);
                    }
                    else
                    {
                        Logger.ErrorFormat("Assembly reference '{0}' for project '{1}' cannot be loaded", assemblyReference.FullName, context.Path);
                        _criticalErrorProvider.RegisterErrorMessage(T(
                            "The assembly reference '{0}' could not be loaded for module '{1}'.\r\n\r\n" +
                            "There are generally a few ways to solve this issue:\r\n" +
                            "1. Install any dependent module.\r\n" +
                            "2. Remove the assembly reference from the project file if it's not needed.\r\n" +
                            "3. Ensure the assembly reference is present in the 'bin' directory of the module.\r\n" +
                            "4. Ensure the assembly reference is present in the 'bin' directory of the application.\r\n" +
                            "5. Specify the strong name of the assembly (name, version, culture, publickey) if the assembly is present in the GAC.",
                            assemblyReference.FullName, moduleName));
                        throw new Exception(string.Format("Assembly reference '{0}' for project '{1}' cannot be loaded", assemblyReference.FullName, context.Path));
                    }
                }

                // compile
                CodeDomProvider provider = new CSharpCodeProvider();
                var compilerParameters = new CompilerParameters {GenerateExecutable = false, GenerateInMemory = true, CompilerOptions = "/optimize"};
                compilerParameters.ReferencedAssemblies.AddRange(references.ToArray());
                var compilerResults = provider.CompileAssemblyFromDom(compilerParameters, compileUnits.ToArray());
                if (compilerResults.Errors.HasErrors)
                {
                    throw new Exception(string.Join("\r\n", compilerResults.Errors.Cast<CompilerError>()));
                }

                var output = compilerResults.CompiledAssembly;
                Logger.InfoFormat("compiled module {0} in assembly {1}", moduleName, output.FullName);
                return output;
            }
            catch (Exception ex)
            {
                Logger.ErrorFormat(ex, "error compiling module \"{0}\" from file \"{1}\":\r\n{2}", moduleName, context.Path, ex.Message);
            }
            return null;
        }

        private CodeCompileUnit CreateCompileUnit(string path)
        {
            var contents = GetContents(path);
            var unit = new CodeSnippetCompileUnit(contents);
            if (!string.IsNullOrEmpty(path))
            {
                unit.LinePragma = new CodeLinePragma(path, 1);
            }
            return unit;
        }

        private string GetContents(string path)
        {
            string contents;
            using (var stream = _pathProvider.OpenFile(path))
            {
                using (var reader = new StreamReader(stream))
                {
                    contents = reader.ReadToEnd();
                }
            }
            return contents;
        }
    }
}